<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>视频循环工具</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/ffmpeg/0.11.6/ffmpeg.min.js"></script>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        font-family: 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', Arial,
          sans-serif;
      }

      :root {
        --primary-bg: #e6e7ee;
        --secondary-bg: #ffffff;
        --shadow-light: #ffffff;
        --shadow-dark: #d1d9e6;
        --accent-color: #667eea;
        --accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        --success-color: #4ecdc4;
        --danger-color: #ff6b6b;
        --warning-color: #feca57;
        --text-primary: #2d3748;
        --text-secondary: #718096;
        --glass-bg: rgba(255, 255, 255, 0.25);
        --glass-border: rgba(255, 255, 255, 0.18);
      }

      body {
        background: var(--primary-bg);
        color: var(--text-primary);
        min-height: 100vh;
        padding: 20px;
        display: flex;
        flex-direction: column;
        align-items: center;
        transition: all 0.3s ease;
        animation: pageLoad 0.8s ease;
      }

      @keyframes pageLoad {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .container {
        width: 100%;
        max-width: 1000px;
        background: var(--primary-bg);
        border-radius: 30px;
        padding: 40px;
        box-shadow: 20px 20px 60px var(--shadow-dark),
          -20px -20px 60px var(--shadow-light);
        margin-top: 20px;
        transition: all 0.3s ease;
        animation: containerFloat 1s ease 0.2s both;
      }

      @keyframes containerFloat {
        from {
          opacity: 0;
          transform: translateY(30px) scale(0.95);
        }
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }

      .container:hover {
        box-shadow: 25px 25px 70px var(--shadow-dark),
          -25px -25px 70px var(--shadow-light);
      }

      header {
        text-align: center;
        margin-bottom: 40px;
        padding: 30px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: headerSlide 0.8s ease 0.4s both;
      }

      @keyframes headerSlide {
        from {
          opacity: 0;
          transform: translateY(-30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      h1 {
        font-size: 3.2rem;
        margin-bottom: 15px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        font-weight: 700;
        animation: titleGlow 3s ease-in-out infinite alternate;
      }

      @keyframes titleGlow {
        from {
          filter: brightness(1);
        }
        to {
          filter: brightness(1.2);
        }
      }

      .subtitle {
        font-size: 1.3rem;
        color: var(--text-secondary);
        margin-bottom: 30px;
        font-weight: 500;
      }

      .panel {
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        transition: all 0.3s ease;
        position: relative;
        overflow: hidden;
        margin-bottom: 30px;
      }

      .panel::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.1),
          transparent
        );
        transition: left 0.8s;
      }

      .panel:hover::before {
        left: 100%;
      }

      .panel:hover {
        transform: translateY(-5px);
        box-shadow: 16px 16px 32px var(--shadow-dark),
          -16px -16px 32px var(--shadow-light);
      }

      .panel-title {
        font-size: 1.6rem;
        margin-bottom: 25px;
        color: var(--accent-color);
        display: flex;
        align-items: center;
        font-weight: 600;
      }

      .panel-title::before {
        content: attr(data-icon);
        margin-right: 12px;
        font-size: 1.8rem;
        animation: iconBounce 2s ease-in-out infinite;
      }

      @keyframes iconBounce {
        0%,
        100% {
          transform: scale(1);
        }
        50% {
          transform: scale(1.1);
        }
      }

      .file-upload {
        border: 3px dashed var(--accent-color);
        border-radius: 25px;
        padding: 50px 40px;
        text-align: center;
        transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        margin-bottom: 25px;
        cursor: pointer;
        background: var(--primary-bg);
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }

      .file-upload:hover {
        border-color: var(--success-color);
        transform: translateY(-5px) scale(1.02);
        box-shadow: inset 10px 10px 20px var(--shadow-dark),
          inset -10px -10px 20px var(--shadow-light),
          0 15px 40px rgba(102, 126, 234, 0.25), 0 5px 15px rgba(0, 0, 0, 0.1);
      }

      .file-upload.dragover {
        border-color: var(--success-color);
        background: rgba(78, 205, 196, 0.05);
        transform: scale(1.05);
        box-shadow: inset 12px 12px 24px var(--shadow-dark),
          inset -12px -12px 24px var(--shadow-light),
          0 20px 50px rgba(78, 205, 196, 0.3);
        animation: dragPulse 1s ease-in-out infinite;
      }

      @keyframes dragPulse {
        0%,
        100% {
          border-color: var(--success-color);
        }
        50% {
          border-color: var(--accent-color);
        }
      }

      .file-upload-content {
        position: relative;
        z-index: 1;
      }

      .file-upload-icon {
        font-size: 3.5rem;
        margin-bottom: 20px;
        display: block;
        animation: iconFloat 3s ease-in-out infinite;
        filter: drop-shadow(0 4px 8px rgba(102, 126, 234, 0.2));
      }

      @keyframes iconFloat {
        0%,
        100% {
          transform: translateY(0);
        }
        50% {
          transform: translateY(-10px);
        }
      }

      .file-upload-text {
        font-size: 1.4rem;
        font-weight: 600;
        color: var(--text-primary);
        margin-bottom: 10px;
        background: var(--accent-gradient);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
      }

      .file-upload-subtext {
        font-size: 1.1rem;
        color: var(--text-secondary);
        margin-bottom: 20px;
        font-weight: 500;
      }

      .file-upload-formats {
        font-size: 0.9rem;
        color: var(--accent-color);
        font-weight: 500;
        padding: 8px 16px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 20px;
        display: inline-block;
        margin-bottom: 20px;
      }

      .file-upload input[type='file'] {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        opacity: 0;
        cursor: pointer;
        z-index: 2;
      }

      .video-preview {
        width: 100%;
        background: var(--primary-bg);
        border-radius: 20px;
        overflow: hidden;
        margin-top: 20px;
        box-shadow: inset 10px 10px 20px var(--shadow-dark),
          inset -10px -10px 20px var(--shadow-light);
        transition: all 0.3s ease;
      }

      .video-preview:hover {
        box-shadow: inset 12px 12px 24px var(--shadow-dark),
          inset -12px -12px 24px var(--shadow-light);
      }

      video {
        width: 100%;
        max-height: 400px;
        display: block;
        border-radius: 15px;
      }

      .options-form {
        display: flex;
        gap: 20px;
        margin-bottom: 25px;
        flex-wrap: wrap;
        align-items: flex-end;
      }

      .form-group {
        flex: 1;
        min-width: 150px;
      }

      label {
        display: block;
        margin-bottom: 10px;
        color: var(--text-secondary);
        font-weight: 500;
        font-size: 0.95rem;
      }

      input,
      select {
        width: 100%;
        padding: 14px 18px;
        background: var(--primary-bg);
        border: none;
        border-radius: 15px;
        color: var(--text-primary);
        font-size: 1rem;
        font-weight: 500;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        transition: all 0.3s ease;
      }

      input:focus,
      select:focus {
        outline: none;
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light),
          0 0 0 3px rgba(102, 126, 234, 0.2);
      }

      button {
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 14px 28px;
        border-radius: 15px;
        cursor: pointer;
        font-weight: 600;
        transition: all 0.3s ease;
        font-size: 1rem;
        flex-shrink: 0;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        position: relative;
        overflow: hidden;
      }

      button::before {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.2),
          transparent
        );
        transition: left 0.5s;
      }

      button:hover::before {
        left: 100%;
      }

      button:hover {
        transform: translateY(-3px);
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
      }

      button:active {
        transform: translateY(0);
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
      }

      button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
        transform: none;
        box-shadow: 4px 4px 8px var(--shadow-dark),
          -4px -4px 8px var(--shadow-light);
      }

      .btn-primary {
        background: var(--accent-gradient);
        color: white;
        padding: 16px 32px;
        font-size: 1.1rem;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
      }

      .btn-primary:hover {
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2),
          0 0 30px rgba(102, 126, 234, 0.3);
      }

      .btn-success {
        background: linear-gradient(
          135deg,
          var(--success-color) 0%,
          #44a08d 100%
        );
        color: white;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light),
          inset 0 1px 0 rgba(255, 255, 255, 0.2);
      }

      .progress-container {
        margin: 30px 0;
        animation: progressSlide 0.8s ease 0.8s both;
      }

      @keyframes progressSlide {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .progress-bar {
        height: 16px;
        background: var(--primary-bg);
        border-radius: 20px;
        overflow: hidden;
        margin-bottom: 15px;
        box-shadow: inset 6px 6px 12px var(--shadow-dark),
          inset -6px -6px 12px var(--shadow-light);
        position: relative;
      }

      .progress-fill {
        height: 100%;
        background: var(--accent-gradient);
        width: 0%;
        transition: width 0.3s ease;
        border-radius: 20px;
        box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
        position: relative;
        overflow: hidden;
      }

      .progress-fill::after {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.3),
          transparent
        );
        animation: progressShine 2s infinite;
      }

      @keyframes progressShine {
        0% {
          left: -100%;
        }
        100% {
          left: 100%;
        }
      }

      .progress-text {
        text-align: center;
        font-size: 1rem;
        color: var(--text-secondary);
        font-weight: 500;
      }

      .actions {
        display: flex;
        justify-content: center;
        gap: 25px;
        margin-top: 30px;
        animation: actionsSlide 0.8s ease 1s both;
      }

      @keyframes actionsSlide {
        from {
          opacity: 0;
          transform: translateY(20px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .result-panel {
        text-align: center;
        animation: resultPanelSlide 0.8s ease 1.2s both;
      }

      @keyframes resultPanelSlide {
        from {
          opacity: 0;
          transform: translateY(30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .status-message {
        min-height: 35px;
        margin: 20px 0;
        color: var(--accent-color);
        font-weight: 600;
        font-size: 1.1rem;
        transition: all 0.3s ease;
      }

      .status-message.processing {
        animation: statusPulse 1.5s ease-in-out infinite;
      }

      @keyframes statusPulse {
        0%,
        100% {
          opacity: 1;
        }
        50% {
          opacity: 0.6;
        }
      }

      .error-message {
        color: var(--danger-color);
        margin: 20px 0;
        min-height: 35px;
        font-weight: 600;
        font-size: 1.1rem;
        animation: errorShake 0.5s ease;
      }

      @keyframes errorShake {
        0%,
        100% {
          transform: translateX(0);
        }
        25% {
          transform: translateX(-5px);
        }
        75% {
          transform: translateX(5px);
        }
      }

      .preview-section {
        display: flex;
        flex-direction: column;
        gap: 15px;
        margin-top: 20px;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        border: 2px solid var(--accent-color);
      }

      .preview-title {
        font-size: 1.1rem;
        font-weight: 600;
        color: var(--text-primary);
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 10px;
      }

      .download-section {
        display: flex;
        flex-direction: column;
        gap: 20px;
        margin-top: 20px;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 25px;
        box-shadow: 12px 12px 24px var(--shadow-dark),
          -12px -12px 24px var(--shadow-light);
        border: 2px solid var(--accent-color);
      }

      .download-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 15px;
        background: var(--primary-bg);
        border-radius: 15px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        transition: all 0.3s ease;
        border: 1px solid rgba(102, 126, 234, 0.1);
      }

      .download-item:hover {
        transform: translateY(-1px);
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        border-color: var(--accent-color);
      }

      .download-title {
        font-size: 1rem;
        font-weight: 600;
        color: var(--text-primary);
        display: flex;
        align-items: center;
        gap: 8px;
      }

      .download-size {
        font-size: 0.9rem;
        color: var(--text-secondary);
        font-weight: 500;
        padding: 6px 12px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 15px;
        border: 1px solid rgba(102, 126, 234, 0.2);
        margin: 0 10px;
      }

      .instructions {
        background: var(--primary-bg);
        border-radius: 25px;
        padding: 30px;
        margin-top: 40px;
        box-shadow: 10px 10px 20px var(--shadow-dark),
          -10px -10px 20px var(--shadow-light);
        animation: instructionsSlide 0.8s ease 1.4s both;
      }

      @keyframes instructionsSlide {
        from {
          opacity: 0;
          transform: translateY(30px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .instructions h3 {
        color: var(--accent-color);
        margin-bottom: 20px;
        font-size: 1.4rem;
        font-weight: 600;
      }

      .instructions ol {
        padding-left: 25px;
        margin-bottom: 20px;
      }

      .instructions li {
        margin-bottom: 12px;
        line-height: 1.7;
        color: var(--text-secondary);
        font-weight: 500;
      }

      .instructions p {
        color: var(--text-secondary);
        font-style: italic;
        font-weight: 500;
        padding: 15px;
        background: rgba(102, 126, 234, 0.1);
        border-radius: 15px;
        border-left: 4px solid var(--accent-color);
      }

      /* 日志相关样式 */
      .log-container {
        margin-top: 20px;
      }

      .log-header {
        display: flex;
        gap: 10px;
        margin-bottom: 15px;
      }

      .log-content {
        background: var(--primary-bg);
        border-radius: 15px;
        padding: 20px;
        max-height: 300px;
        overflow-y: auto;
        box-shadow: inset 8px 8px 16px var(--shadow-dark),
          inset -8px -8px 16px var(--shadow-light);
        font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
        font-size: 0.9em;
        line-height: 1.4;
        color: var(--text-secondary);
        white-space: pre-wrap;
        word-wrap: break-word;
      }

      .log-entry {
        margin-bottom: 8px;
        padding: 5px 0;
        border-bottom: 1px solid rgba(209, 217, 230, 0.3);
      }

      .log-entry:last-child {
        border-bottom: none;
      }

      .log-timestamp {
        color: var(--accent-color);
        font-weight: 600;
        margin-right: 10px;
      }

      .log-level {
        display: inline-block;
        padding: 2px 8px;
        border-radius: 12px;
        font-size: 0.8em;
        font-weight: 600;
        margin-right: 10px;
        min-width: 50px;
        text-align: center;
      }

      .log-level.info {
        background: rgba(102, 126, 234, 0.2);
        color: var(--accent-color);
      }

      .log-level.success {
        background: rgba(78, 205, 196, 0.2);
        color: var(--success-color);
      }

      .log-level.warning {
        background: rgba(254, 202, 87, 0.2);
        color: var(--warning-color);
      }

      .log-level.error {
        background: rgba(255, 107, 107, 0.2);
        color: var(--danger-color);
      }

      .btn-secondary {
        background: var(--primary-bg);
        color: var(--text-primary);
        border: none;
        padding: 12px 24px;
        border-radius: 15px;
        font-size: 1em;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        box-shadow: 8px 8px 16px var(--shadow-dark),
          -8px -8px 16px var(--shadow-light);
        text-decoration: none;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
      }

      .btn-secondary:hover {
        box-shadow: 4px 4px 8px var(--shadow-dark),
          -4px -4px 8px var(--shadow-light);
        transform: translateY(2px);
      }

      .btn-secondary:active {
        box-shadow: inset 4px 4px 8px var(--shadow-dark),
          inset -4px -4px 8px var(--shadow-light);
        transform: translateY(4px);
      }

      footer {
        margin-top: 40px;
        text-align: center;
        color: var(--text-secondary);
        font-size: 0.95rem;
        padding: 25px;
        background: var(--primary-bg);
        border-radius: 20px;
        box-shadow: 6px 6px 12px var(--shadow-dark),
          -6px -6px 12px var(--shadow-light);
        animation: footerFade 0.8s ease 1.6s both;
      }

      @keyframes footerFade {
        from {
          opacity: 0;
        }
        to {
          opacity: 1;
        }
      }

      @media (max-width: 768px) {
        .container {
          padding: 20px;
          margin: 10px;
        }

        h1 {
          font-size: 2.5rem;
        }

        .subtitle {
          font-size: 1.1rem;
        }

        .options-form {
          flex-direction: column;
        }

        .form-group {
          min-width: 100%;
        }

        .actions {
          flex-direction: column;
          align-items: center;
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <header>
        <h1>🔄 视频循环工具</h1>
        <p class="subtitle">将视频文件循环指定次数，支持多种视频格式</p>
      </header>

      <div class="panel">
        <h2 class="panel-title" data-icon="🎬">视频上传</h2>
        <div class="file-upload" id="videoUpload">
          <div class="file-upload-content">
            <span class="file-upload-icon">🎬</span>
            <div class="file-upload-text">点击或拖拽视频文件到此处</div>
            <div class="file-upload-subtext">
              支持拖拽上传，最大文件大小 1GB
            </div>
            <div class="file-upload-formats">
              支持格式：MP4, AVI, MOV, MKV, WMV, FLV, WEBM
            </div>
          </div>
          <input
            type="file"
            id="videoInput"
            accept=".mp4,.avi,.mov,.mkv,.wmv,.flv,.webm"
          />
        </div>

        <div class="video-preview" id="videoPreview" style="display: none">
          <video controls id="videoPlayer"></video>
        </div>
      </div>

      <div class="panel">
        <h2 class="panel-title" data-icon="⚙️">循环设置</h2>
        <div class="options-form">
          <div class="form-group">
            <label for="loopCount">循环次数</label>
            <input
              type="number"
              id="loopCount"
              min="2"
              max="20"
              value="3"
              placeholder="输入循环次数"
            />
          </div>
          <div class="form-group">
            <label for="outputFormat">输出格式</label>
            <select id="outputFormat">
              <option value="mp4">MP4</option>
              <option value="avi">AVI</option>
              <option value="mov">MOV</option>
              <option value="mkv">MKV</option>
              <option value="webm">WEBM</option>
            </select>
          </div>
          <div class="form-group">
            <label for="quality">视频质量</label>
            <select id="quality">
              <option value="medium">中等质量 (快速)</option>
              <option value="high" selected>高质量 (推荐)</option>
              <option value="veryhigh">最高质量 (慢速)</option>
            </select>
          </div>
        </div>
      </div>

      <div class="actions">
        <button class="btn-primary" id="processBtn" disabled>
          🔄 开始循环处理
        </button>
      </div>

      <div
        class="progress-container"
        id="progressContainer"
        style="display: none"
      >
        <div class="progress-bar">
          <div class="progress-fill" id="progressFill"></div>
        </div>
        <div class="progress-text" id="progressText">准备中...</div>
      </div>

      <div class="status-message" id="statusMessage"></div>
      <div class="error-message" id="errorMessage"></div>

      <div class="result-panel" id="resultPanel" style="display: none">
        <div class="preview-section">
          <div class="preview-title">🎬 预览循环视频</div>
          <video
            id="resultVideo"
            controls
            style="width: 100%; margin: 10px 0; border-radius: 15px"
          >
            您的浏览器不支持视频播放。
          </video>
        </div>
        <div class="download-section">
          <div class="download-item">
            <div class="download-title">🎬 循环视频文件</div>
            <div class="download-size" id="downloadSize">-</div>
            <button class="btn-success" id="downloadBtn">下载文件</button>
          </div>
          <div class="download-item">
            <div class="download-title">📋 处理日志</div>
            <div class="download-size" id="logSize">-</div>
            <button class="btn-secondary" id="downloadLogBtn">下载日志</button>
          </div>
        </div>
      </div>

      <div class="panel" id="logPanel" style="display: none">
        <h2 class="panel-title" data-icon="📋">处理日志</h2>
        <div class="log-container">
          <div class="log-header">
            <button class="btn-secondary" id="toggleLogBtn">显示日志</button>
            <button class="btn-secondary" id="clearLogBtn">清空日志</button>
          </div>
          <div class="log-content" id="logContent" style="display: none"></div>
        </div>
      </div>

      <div class="instructions">
        <h3>使用说明</h3>
        <ol>
          <li>选择或拖拽视频文件到上传区域</li>
          <li>设置循环次数（2-20次）</li>
          <li>选择输出格式和视频质量</li>
          <li>点击"开始循环处理"按钮</li>
          <li>等待处理完成后下载结果文件</li>
        </ol>
        <p>
          注意：处理时间取决于文件大小、循环次数和视频质量。所有处理都在本地完成，保护您的隐私。
        </p>
      </div>

      <footer>
        <p>🔒 本地处理，保护隐私 | 🚀 基于 FFmpeg.wasm 技术</p>
      </footer>
    </div>

    <script>
      let ffmpeg = null
      let videoFile = null
      let isProcessing = false
      let processingLogs = []
      let logVisible = false

      // 初始化 FFmpeg
      async function initFFmpeg() {
        if (!ffmpeg) {
          ffmpeg = FFmpeg.createFFmpeg({
            log: true,
          })
        }

        if (!ffmpeg.isLoaded()) {
          await ffmpeg.load()
        }
      }

      // 文件上传处理
      function setupFileUpload() {
        const uploadArea = document.getElementById('videoUpload')
        const fileInput = document.getElementById('videoInput')
        const videoPreview = document.getElementById('videoPreview')
        const videoPlayer = document.getElementById('videoPlayer')
        const processBtn = document.getElementById('processBtn')

        // 点击上传
        uploadArea.addEventListener('click', (e) => {
          if (e.target !== fileInput) {
            e.preventDefault()
            fileInput.click()
          }
        })

        // 文件选择
        fileInput.addEventListener('change', handleFileSelect)

        // 拖拽上传
        uploadArea.addEventListener('dragover', (e) => {
          e.preventDefault()
          uploadArea.classList.add('dragover')
        })

        uploadArea.addEventListener('dragleave', (e) => {
          e.preventDefault()
          uploadArea.classList.remove('dragover')
        })

        uploadArea.addEventListener('drop', (e) => {
          e.preventDefault()
          uploadArea.classList.remove('dragover')
          const files = e.dataTransfer.files
          if (files.length > 0) {
            handleFile(files[0])
          }
        })

        function handleFileSelect(e) {
          const file = e.target.files[0]
          if (file) {
            handleFile(file)
          }
          // 清空input的值，确保相同文件可以重复选择
          fileInput.value = ''
        }

        function handleFile(file) {
          addLog('info', `开始处理文件: ${file.name}`)

          // 检查文件大小（1GB限制）
          if (file.size > 1024 * 1024 * 1024) {
            showError('文件大小超过1GB限制')
            return
          }

          // 检查文件类型
          const allowedTypes = [
            'video/mp4',
            'video/avi',
            'video/quicktime',
            'video/x-msvideo',
            'video/x-ms-wmv',
            'video/x-flv',
            'video/webm',
            'video/x-matroska',
          ]
          if (
            !allowedTypes.includes(file.type) &&
            !file.name.match(/\.(mp4|avi|mov|mkv|wmv|flv|webm)$/i)
          ) {
            showError('不支持的视频格式')
            return
          }

          videoFile = file
          addLog(
            'success',
            `文件验证通过: ${file.name} (${formatFileSize(file.size)})`
          )

          // 显示视频预览
          const url = URL.createObjectURL(file)
          videoPlayer.src = url
          videoPreview.style.display = 'block'
          addLog('info', '视频预览已加载')

          // 启用处理按钮
          processBtn.disabled = false

          // 显示日志面板
          document.getElementById('logPanel').style.display = 'block'

          showStatus(`已选择文件: ${file.name} (${formatFileSize(file.size)})`)
          clearError()
        }
      }

      // 处理视频循环
      async function processVideoLoop() {
        if (!videoFile || isProcessing) return

        isProcessing = true
        const processBtn = document.getElementById('processBtn')
        const progressContainer = document.getElementById('progressContainer')
        const resultPanel = document.getElementById('resultPanel')

        processBtn.disabled = true
        progressContainer.style.display = 'block'
        resultPanel.style.display = 'none'

        try {
          showStatus('正在初始化 FFmpeg...')
          updateProgress(0, '初始化中...')

          await initFFmpeg()

          showStatus('正在处理视频文件...')
          updateProgress(10, '读取视频文件...')

          // 读取文件
          const videoData = new Uint8Array(await videoFile.arrayBuffer())
          const inputFileName = `input.${getFileExtension(videoFile.name)}`

          ffmpeg.FS('writeFile', inputFileName, videoData)

          updateProgress(20, '分析视频信息...')

          const loopCount = parseInt(document.getElementById('loopCount').value)
          const outputFormat = document.getElementById('outputFormat').value
          const quality = document.getElementById('quality').value

          const outputFileName = `output.${outputFormat}`

          updateProgress(30, `开始循环处理 (${loopCount}次)...`)

          // 构建 FFmpeg 命令 - 使用正确的循环方法
          const args = [
            '-stream_loop',
            (loopCount - 1).toString(),
            '-i',
            inputFileName,
            '-c',
            'copy',
          ]

          // 根据质量设置和输出格式调整编码参数
          const inputExt = getFileExtension(videoFile.name).toLowerCase()
          const needsReencode =
            outputFormat !== inputExt || quality !== 'medium'

          if (needsReencode) {
            // 需要重新编码时移除 -c copy 并添加编码设置
            const copyIndex = args.indexOf('-c')
            if (copyIndex !== -1) {
              args.splice(copyIndex, 2) // 移除 '-c', 'copy'
            }

            // 添加质量设置
            if (quality === 'medium') {
              args.push('-crf', '28', '-preset', 'fast')
            } else if (quality === 'high') {
              args.push('-crf', '23', '-preset', 'medium')
            } else if (quality === 'veryhigh') {
              args.push('-crf', '18', '-preset', 'slow')
            }

            // 添加编解码器设置
            if (outputFormat === 'mp4') {
              args.push('-c:v', 'libx264', '-c:a', 'aac')
            } else if (outputFormat === 'webm') {
              args.push('-c:v', 'libvpx-vp9', '-c:a', 'libopus')
            } else if (outputFormat === 'avi') {
              args.push('-c:v', 'libx264', '-c:a', 'mp3')
            } else if (outputFormat === 'mov') {
              args.push('-c:v', 'libx264', '-c:a', 'aac')
            } else if (outputFormat === 'mkv') {
              args.push('-c:v', 'libx264', '-c:a', 'aac')
            }
          }

          args.push('-y', outputFileName)

          // 设置进度监听
          ffmpeg.setLogger(({ message }) => {
            const timeMatch = message.match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/)
            if (timeMatch) {
              const timeStr = timeMatch[1]
              updateProgress(50, `处理中... time=${timeStr}`)
            }
          })

          ffmpeg.setProgress(({ ratio }) => {
            const progress = Math.min(Math.max(30 + ratio * 60, 30), 90)
            // 只更新进度条，不更新文字
            document.getElementById('progressFill').style.width = `${progress}%`
          })

          await ffmpeg.run(...args)

          updateProgress(95, '准备下载文件...')

          // 读取输出文件
          const outputData = ffmpeg.FS('readFile', outputFileName)
          addLog('info', `输出文件大小: ${formatFileSize(outputData.length)}`)

          // 创建下载链接
          const blob = new Blob([outputData.buffer], {
            type: `video/${outputFormat}`,
          })
          const url = URL.createObjectURL(blob)
          addLog('success', '视频文件准备完成，可以下载')

          // 设置预览和下载
          const resultVideo = document.getElementById('resultVideo')
          const downloadBtn = document.getElementById('downloadBtn')
          const downloadSize = document.getElementById('downloadSize')
          const downloadLogBtn = document.getElementById('downloadLogBtn')
          const logSize = document.getElementById('logSize')

          // 设置视频预览
          resultVideo.src = url
          addLog('info', '视频预览已设置')

          downloadBtn.onclick = () => {
            const a = document.createElement('a')
            a.href = url
            a.download = `${getFileNameWithoutExt(
              videoFile.name
            )}_loop_${loopCount}.${outputFormat}`
            a.click()
            addLog('info', '视频文件下载已开始')
          }

          // 设置日志下载
          downloadLogBtn.onclick = downloadLogs

          // 更新文件大小显示
          downloadSize.textContent = formatFileSize(blob.size)
          const logText = processingLogs
            .map(
              (log) =>
                `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`
            )
            .join('\n')
          const logBlob = new Blob([logText], {
            type: 'text/plain;charset=utf-8',
          })
          logSize.textContent = formatFileSize(logBlob.size)

          // 清理临时文件
          try {
            ffmpeg.FS('unlink', inputFileName)
            ffmpeg.FS('unlink', outputFileName)
            addLog('info', '临时文件已清理')
          } catch (e) {
            console.warn('清理临时文件失败:', e)
            addLog('warning', `清理临时文件失败: ${e.message}`)
          }

          updateProgress(100, '处理完成！')
          showStatus(`视频循环处理完成！循环次数: ${loopCount}`)
          addLog(
            'success',
            `处理完成！输入: ${videoFile.name}, 输出: ${getFileNameWithoutExt(
              videoFile.name
            )}_loop_${loopCount}.${outputFormat}`
          )
          resultPanel.style.display = 'block'
        } catch (error) {
          console.error('处理失败:', error)
          showError(`处理失败: ${error.message}`)
          updateProgress(0, '处理失败')
        } finally {
          isProcessing = false
          processBtn.disabled = false
          setTimeout(() => {
            progressContainer.style.display = 'none'
          }, 2000)
        }
      }

      // 日志记录函数
      function addLog(level, message) {
        const timestamp = new Date().toLocaleString('zh-CN', {
          year: 'numeric',
          month: '2-digit',
          day: '2-digit',
          hour: '2-digit',
          minute: '2-digit',
          second: '2-digit',
        })

        const logEntry = {
          timestamp,
          level,
          message,
        }

        processingLogs.push(logEntry)
        updateLogDisplay()

        // 自动滚动到底部
        const logContent = document.getElementById('logContent')
        if (logContent && logVisible) {
          setTimeout(() => {
            logContent.scrollTop = logContent.scrollHeight
          }, 100)
        }
      }

      function updateLogDisplay() {
        const logContent = document.getElementById('logContent')
        if (!logContent) return

        logContent.innerHTML = processingLogs
          .map(
            (log) =>
              `<div class="log-entry">
            <span class="log-timestamp">${log.timestamp}</span>
            <span class="log-level ${
              log.level
            }">${log.level.toUpperCase()}</span>
            <span class="log-message">${log.message}</span>
          </div>`
          )
          .join('')
      }

      function clearLogs() {
        processingLogs = []
        updateLogDisplay()
        document.getElementById('logSize').textContent = '-'
      }

      function downloadLogs() {
        if (processingLogs.length === 0) {
          showError('没有日志可以下载')
          return
        }

        const logText = processingLogs
          .map(
            (log) =>
              `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`
          )
          .join('\n')

        const blob = new Blob([logText], { type: 'text/plain;charset=utf-8' })
        const url = URL.createObjectURL(blob)

        const a = document.createElement('a')
        a.href = url
        a.download = `video-loop-log-${new Date()
          .toISOString()
          .slice(0, 19)
          .replace(/:/g, '-')}.txt`
        a.click()

        URL.revokeObjectURL(url)
      }

      function toggleLogVisibility() {
        const logContent = document.getElementById('logContent')
        const toggleBtn = document.getElementById('toggleLogBtn')

        logVisible = !logVisible
        logContent.style.display = logVisible ? 'block' : 'none'
        toggleBtn.textContent = logVisible ? '隐藏日志' : '显示日志'

        if (logVisible) {
          setTimeout(() => {
            logContent.scrollTop = logContent.scrollHeight
          }, 100)
        }
      }

      // 工具函数
      function getFileExtension(filename) {
        return filename.split('.').pop().toLowerCase()
      }

      function getFileNameWithoutExt(filename) {
        return filename.substring(0, filename.lastIndexOf('.'))
      }

      function formatFileSize(bytes) {
        if (bytes === 0) return '0 Bytes'
        const k = 1024
        const sizes = ['Bytes', 'KB', 'MB', 'GB']
        const i = Math.floor(Math.log(bytes) / Math.log(k))
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
      }

      function updateProgress(percent, text) {
        const progressFill = document.getElementById('progressFill')
        const progressText = document.getElementById('progressText')
        progressFill.style.width = percent + '%'
        progressText.textContent = text

        // 添加进度日志
        if (percent === 0) {
          addLog('error', text)
        } else if (percent === 100) {
          addLog('success', text)
        } else {
          addLog('info', text)
        }
      }

      function showStatus(message) {
        const statusElement = document.getElementById('statusMessage')
        statusElement.textContent = message
        statusElement.className = 'status-message processing'
        addLog('info', message)
      }

      function showError(message) {
        const errorElement = document.getElementById('errorMessage')
        errorElement.textContent = message
        setTimeout(() => {
          errorElement.textContent = ''
        }, 5000)
        addLog('error', message)
      }

      function clearError() {
        document.getElementById('errorMessage').textContent = ''
      }

      // 初始化
      document.addEventListener('DOMContentLoaded', () => {
        setupFileUpload()

        // 处理按钮事件
        document
          .getElementById('processBtn')
          .addEventListener('click', processVideoLoop)

        // 日志相关事件
        document
          .getElementById('toggleLogBtn')
          .addEventListener('click', toggleLogVisibility)

        document
          .getElementById('clearLogBtn')
          .addEventListener('click', clearLogs)

        document
          .getElementById('downloadLogBtn')
          .addEventListener('click', downloadLogs)

        // 预加载 FFmpeg
        initFFmpeg()
          .then(() => {
            addLog('success', 'FFmpeg 初始化完成')
          })
          .catch((error) => {
            console.error(error)
            addLog('error', `FFmpeg 初始化失败: ${error.message}`)
          })

        addLog('info', '视频循环工具已启动')
      })
    </script>
  </body>
</html>